<?php


namespace App\Helpers;


use App\Exceptions\BaseException;
use App\Library\LogLib;
use Throwable;

/**
 * 日志助手类
 * Class LogHelper
 *
 * @package App\Helpers
 * @author  kingsg
 */
class LogHelper extends Helper
{
	public static function debug(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	public static function info(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	public static function notice(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	public static function warning(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	/**
	 * 错误日志
	 *
	 * @param string       $mark
	 * @param array|string $message
	 */
	public static function error(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	public static function critical(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	public static function alert(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	public static function emergency(string $mark, $message)
	{
		static::addLog(__FUNCTION__, $mark, $message);
	}

	/**
	 * 异常日志
	 *
	 * @param Throwable    $ex
	 * @param array|string $otherMsg
	 */
	public static function exLog(Throwable $ex, $otherMsg = '')
	{
		$mark = static::getExceptionClassName($ex);
		try {
			// 加工日志信息
			$logInfo = static::makeLogInfo($ex);
			if ($otherMsg) {
				if (is_array($otherMsg)) {
					$otherMsg = json_encode($otherMsg, JSON_UNESCAPED_UNICODE);
				}
				$logInfo['message'] .= ' | ' . $otherMsg;
			}
		} catch (Throwable $exception) {
			$logInfo = 'exLog底层记录异常日志失败 | ' . $exception->getMessage();
		} finally {
			// 记录日志
			static::error($mark, $logInfo);
			// 发送警告消息
			self::isSendWarn($ex) && static::sendWarn($mark, $logInfo);
		}
	}


	/**
	 * 记录日志
	 *
	 * @param string $type
	 * @param string $mark
	 * @param        $message
	 */
	private static function addLog(string $type, string $mark, $message)
	{
		try {
			LogLib::addLog($mark, $type, $message);
		} catch (Throwable $ex) {
			// @tips 严重事件,文件日志写入失败！！！请及时查看 |
			// 此处「不能」通过GuzzleHttpTrait：发送报警信息： 因为 GuzzleHttpTrait 里面有记日志操作会造成循环 !!!
		}
	}

	/**
	 * 发送警告消息
	 *
	 * @param $mark
	 * @param $message
	 */
	private static function sendWarn($mark, $message)
	{
		// 发送钉钉消息
//		WatchDogHelper::sendText(WatchDogHelper::makeMsg($mark, $message));
	}

	/**
	 * 加工日志信息
	 *
	 * @param Throwable $ex
	 * @return array
	 */
	private static function makeLogInfo(Throwable $ex): array
	{
		// 获取日志记录在文件中的位置
		static $number = 0;
		// 获取调用堆栈
		$debugTraces = static::getTraces($ex);
		// 去掉项目目录路径
		$file = $ex->getFile();
		if (stripos($file, base_path()) !== false) {
			$file = substr($file, strlen(base_path()));
		}

		$exCode = $ex->getCode();
		// 日志记录，保持和接口输出一致
		isSubClassOfBaseException($ex) && $exCode = $ex->getFullCode($exCode);

		// 参数
		$paramsAll = MvcHelper::makeRequestParams();

		return [
			"message"    => $ex->getMessage(),    // 错误信息
			"file"       => $file,                 // 报错的文件
			"line"       => $ex->getLine(),   // 报错的文件的行号
			"code"       => $exCode,   // 错误码
			'tracesTime' => getNowDate(),   // 记录日志的时间
			'traces'     => $debugTraces,            // 堆栈信息由近至远[“请求顺序”：“文件路径(行号)”：调用的方法()]
			'tracesNo'   => 'No.' . ($number++),                // 记录日志的顺序号（多次记录这个可以作为序号）
			'srvIp'      => ($_SERVER['SERVER_ADDR'] ?? ''),   // 机器ID:本机ID
			'ip'         => ip($_SERVER), // 请求IP
			'sysParams'  => $paramsAll['sysParams'], //系统参数
			'params'     => $paramsAll['params'], //业务参数
			'userId'     => $paramsAll['userId'], //用户ID
		];

	}

	/**
	 * 获取日志记录在文件中的位置
	 *
	 * @param Throwable $exception
	 * @return array
	 */
	private static function getTraces(Throwable $exception): array
	{
		if (!$exception || !$exception->getFile()) {
			return [];
		}

		$lines = [];
		// 项目目录路径的字符串长度
		$basePathLen = strlen(base_path());
		// 异常堆栈
		$traces = $exception->getTrace();

		$apiFlag   = false;
		$tracesLen = count($traces);
		for ($i = ($tracesLen - 1); $i >= 0; $i--) {
			// 倒叙取值
			$trace = $traces[$i];
			if (!isset($trace['file'])) {
				continue;
			}

			// 忽略composer组件
			if (stripos($trace['file'], 'vendor/') !== false) {
				continue;
			}

			if ($apiFlag === false) {
				// 忽略业务接口之前的框架组件：
				if (stripos($trace['file'], '/app/Http/Controllers/Api') === false) {
					continue;
				}

				$apiFlag = true;
			}

			// 去掉项目目录路径
			if (stripos($trace['file'], base_path()) !== false) {
				$trace['file'] = substr($trace['file'], $basePathLen);
			}

			// 拼装友好的字符串
			$lines[] = ($trace['file'] ?? "") . '(' . ($trace['line'] ?? "") . ') ' . ($trace['type'] ?? "") . ($trace['function'] ?? "") . '()';
		}
		// 排序：按调用的顺序从远到近： 例：当前(n)<-过程(...)<-过程(2)<-过程(1)<-入口(0)
		krsort($lines);

		return $lines;
	}

	/**
	 * 获取异常类的名称，转换为小写
	 *
	 * @param Throwable $exception
	 * @return array|string|string[]
	 */
	public static function getExceptionClassName(Throwable $exception)
	{
		$logMark = explode("\\", get_class($exception));

		return str_replace('exception', '', strtolower($logMark[count($logMark) - 1]));
	}


	/**
	 * 是否发送钉钉消息
	 *
	 * @param Throwable $exception
	 *
	 * @return bool
	 */
	public static function isSendWarn(Throwable $exception): bool
	{
		$exCode       = $exception->getCode();
		$sendDingTalk = true;
		if (isSubClassOfBaseException($exception)) {
			$sendDingTalk = substr(strval($exCode),
					BaseException::SEND_MSG_POSITION_OF_CODE,
					1) == BaseException::SEND_MSG_CODE_YES;
		}

		return $sendDingTalk;
	}
}
